# -*- coding: utf-8 -*-
from django.db import models
from django.contrib.auth.models import User
from skoolmaster.syllabus.models import Subject, SchoolYear, StudentSubjectMetadata
from skoolmaster.general.fields import MMIntegerField
from datetime import timedelta, datetime, date

class TooManyApplications(Exception):
  pass

class TooManyApplicationsThisYear(Exception):
  pass

class AlreadyApplied(Exception):
  pass

class RecentApplicationTooClose(Exception):
  pass

class ApplicationDateHasExpired(Exception):
  pass

class MustPay(Exception):
  pass

class Exam(models.Model):
  """
  This model represents an exam.
  """
  subject = models.ForeignKey(Subject, verbose_name = 'Predmet', related_name = 'exams')
  school_year = models.ForeignKey(SchoolYear, verbose_name = u'Šolsko leto', related_name = 'exams')
  date = models.DateTimeField('Datum in ura')
  applications = models.ManyToManyField(User, verbose_name = 'Prijave', through = 'ExamApplication')
  location = models.CharField(max_length = 20)
  examiners = models.ManyToManyField(User, related_name = 'owned_exams')
  max_points = models.IntegerField(default = 100)
  locked = models.BooleanField(default = False)
  has_grades = models.BooleanField(default = False)
  
  class Meta:
    ordering = ['date', 'subject']
    get_latest_by = 'date'
  
  def check_can_take(self, student, multiple_exceptions = False):
    """
    Raises an exception if the specified student is not allowed to take
    this exam.
    
    @param student: A valid student instance
    @param multiple_exceptions: Set to true to return a list of exceptions
    """
    errors = []
    def add_or_raise(e):
      if multiple_exceptions:
        errors.append(e)
      else:
        raise e
    
    if student.get_profile().enrollment_forms.filter(school_year = self.school_year).count() == 0:
      add_or_raise(MustPay)
    
    if self.date.date() < date.today() + timedelta(days = 3) or self.locked:
      add_or_raise(ApplicationDateHasExpired)
    
    if StudentSubjectMetadata.get_times_taken(student, self.subject, self.school_year) >= 3:
      add_or_raise(TooManyApplicationsThisYear)
    
    if StudentSubjectMetadata.get_times_taken(student, self.subject) >= 6:
      add_or_raise(TooManyApplications)
      
    if StudentSubjectMetadata.get_times_taken(student, self.subject) >= 3:
      add_or_raise(MustPay)      
    
    if student in self.applications.all():
      add_or_raise(AlreadyApplied)
    
    if ExamApplication.objects.filter(
        student = student, 
        exam__subject = self.subject, 
        grade_exam__isnull = True,
        grade_tut__isnull = True
      ).count() > 0:
      add_or_raise(AlreadyApplied)
    
    if ExamApplication.objects.filter(student = student, exam__subject = self.subject, \
                                      exam__date__gte = self.date - timedelta(days = 14)).count() > 0:
      add_or_raise(RecentApplicationTooClose)
    
    return errors
  
  def can_signout(self):
    """
    Returns true if the student can sign out of this exam.
    """
    return self.date.date() >= date.today() + timedelta(days = 3)


      
  
  def __unicode__(self):
    """
    Returns a string representation of this model.
    """
    return u'%s.%s.%s %s' % (self.date.day, self.date.month, self.date.year, self.subject)

class ExamApplication(models.Model):
  """
  This model represents an exam application by a student.
  """
  student = models.ForeignKey(User, verbose_name = u'Študent', related_name = 'exam_applications')
  exam = models.ForeignKey(Exam, verbose_name = 'Izpit')
  seq_no = models.IntegerField()
  grade_exam = MMIntegerField('Ocena izpita', min_value = 5, max_value = 10, blank = True, null = True)
  grade_tut = MMIntegerField('Ocena vaj', min_value = 5, max_value = 10, blank = True, null = True)
  points = models.IntegerField(null = True, blank = True)
  must_pay = models.BooleanField(default = False)
  passed = models.BooleanField()

  def __unicode__(self):
    """
    Returns a string representation of this model.
    """
    return u'%s - %s' % (self.exam, self.student)

  def get_school_year(self):
    """
    Returns the school year when the student had the subject.
    """
    return unicode(SchoolYear.objects.filter(
      enrollments__student = self.student.get_profile(),
      enrollments__subjects = self.exam.subject
    ).latest())

  def get_number_of_takes(self):
    """
    Returns the number of takes for this subject/student combo.
    """
    return StudentSubjectMetadata.get_times_taken(self.student, self.exam.subject) 

  def get_number_of_subtractions(self):
    """
    Returns the number of subtractions for this subject/student combo.
    """
    return StudentSubjectMetadata.get_times_taken(self.student, self.exam.subject, subtraction = True)
  
  @staticmethod
  def _before_save_callback(sender, **kwargs):
    """
    Handler for pre_save signal.
    """
    app = kwargs['instance']
    if not app.seq_no:
      try:
        app.seq_no = ExamApplication.objects.filter(
          student = app.student,
          exam__subject = app.exam.subject
        ).order_by("-seq_no").values_list('seq_no', flat = True)[0] + 1
      except IndexError:
        app.seq_no = 1
    
    # Update passed attribute depending on subject grade mode
    app.passed = True
    if app.exam.subject.has_grade_exam() and app.grade_exam <= 5:
      app.passed = False
    
    if app.exam.subject.has_grade_tut() and app.grade_tut <= 5:
      app.passed = False
  
  @staticmethod
  def _on_save_callback(sender, **kwargs):
    """
    Handler for post_save signal.
    """
    if not kwargs.get('created', False):
      return
    
    # Ensure metadata consistency
    app = kwargs['instance']
    StudentSubjectMetadata.recount(app.student, app.exam.subject)
  
  @staticmethod
  def _on_delete_callback(sender, **kwargs):
    """
    Handler for post_delete signal.
    """
    app = kwargs['instance']
    StudentSubjectMetadata.recount(app.student, app.exam.subject)

models.signals.pre_save.connect(ExamApplication._before_save_callback, sender = ExamApplication)
models.signals.post_save.connect(ExamApplication._on_save_callback, sender = ExamApplication)
models.signals.post_delete.connect(ExamApplication._on_delete_callback, sender = ExamApplication)

